![]() |
![]() |
|
Im Folgenden werde ich Ihnen beide Alternativlösungen zum CommandBuilder vorstellen. 26.7.3 Aktualisieren mit »ExecuteNonQuery«
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UPDATE authors SET au_id = x, au_lname = y, au_fname = z, ... |
| WHERE au_id = abcd |
Findet der Aktualisierungsprozess in der Originaltabelle authors den Datensatz mit dem Primärschlüssel abcd, trägt er alle hinter SET aufgeführten Spalteninhalte ein. Dabei interessiert nicht im Geringsten, ob sich die Inhalte seit dem Generieren des DataSets auf Benutzerseite geändert haben oder nicht. Beachten Sie, dass sich dieses UPDATE-Statement deutlich von dem, was ein CommandBuilder erzeugen würde, unterscheidet.
Das Vorhaben war, jede geänderte Datenzeile mit ExecuteNonQuery zu übermitteln. Also benötigen wir zuerst ein Command-Objekt, das die parametrisierte Abfrage beschreibt. Dafür schreiben wir eine Methode.
| ' UpdateCommand erzeugen |
| Public Function GetUpdateCommand(ByVal con As SqlConnection) _ |
| As SqlCommand |
| Dim strSQL As String = "UPDATE authors SET au_id=@IDNew, " |
| strSQL &= "au_lname=@Zuname, au_fname=@Vorname WHERE au_id=@ID" |
| Dim cmd As SqlCommand = New SqlCommand(strSQL, con) |
| ' die Parameter der Parameters-Auflistung hinzufügen |
| cmd.Parameters.Add("@IDNew", SqlDbType.VarChar, 11) |
| cmd.Parameters.Add("@Zuname", SqlDbType.VarChar, 40) |
| cmd.Parameters.Add("@Vorname", SqlDbType.VarChar, 20) |
| cmd.Parameters.Add("@ID", SqlDbType.VarChar, 11) |
| Return cmd |
| End Function |
Um den Code übersichtlich zu halten, enthält UPDATE nur die Angabe der drei Spalten au_id, au_lname und au_fname. Diese werden, wie auch die Angabe des Suchkriteriums, als Parameter des Command-Objekts definiert. Die Methode GetUpdateCommand liefert dem aufrufenden Code ein SqlCommand als Rückgabewert, das vollständig ausgebildet ist.
| myCommand.ExecuteNonQuery() |
aktualisiert werden kann, müssen die Parameter gefüllt werden. Dazu dient eine eigene Methode. Weil nach dem Füllen der Parameter das Command-Objekt ausführungsfertig ist, kann sogar schon innerhalb der Methode ExecuteNonQuery aufgerufen werden. Dazu muss der Methode neben dem von GetUpdateCommand generierten Command-Objekt nur die zu aktualisierende Datenzeile bekannt sein. Beide übergeben wir der Parameterliste der Methode, die wir SubmitUpdateRow nennen.
Der Aufruf von ExecuteNonQuery liefert eine Zahl zurück, die Auskunft darüber gibt, wie viele Datenzeilen aktualisiert werden konnten. Darauf wollen wir nicht verzichten, denn Sie kann uns dabei behilflich sein, etwaige Konflikte, die beim Aktualisieren auftreten, zu behandeln. Stellen Sie sich dazu nur vor, ein anderer Anwender hätte zwischenzeitlich den betreffenden Datensatz gelöscht. Der Rückgabewert von ExecuteNonQuery und damit auch unserer benutzerdefinierten Methode wäre 0.
| Public Function SubmitUpdatedRow(ByVal cmd As SqlCommand, _ |
| ByVal row As DataRow) As Integer |
| ' Parameter füllen |
| cmd.Parameters("@IDNew").Value = row("au_id", _ |
| DataRowVersion.Current) |
| cmd.Parameters("@Zuname").Value = row("au_lname", _ |
| DataRowVersion.Current) |
| cmd.Parameters("@Vorname").Value = row("au_fname", _ |
| DataRowVersion.Current) |
| cmd.Parameters("@ID").Value = row("au_id", _ |
| DataRowVersion.Original) |
| ' Anzahl der betroffenen Zeilen zurückliefern |
| Return cmd.ExecuteNonQuery() |
| End Function |
Beim Füllen der Parameter müssen Sie bedenken, welche Datenversion Sie in den jeweiligen Parameter eintragen müssen. Die hinter SET aufgeführten Spalten beschreiben die zu ändernden Werte. Es sind also die, die unter DataRowVersion.Current der entsprechenden Datenzeile für die Spalte zu finden sind. Übergeben Sie der Eigenschaft Value des Parameters beispielsweise mit
| row("au_fname") |
einen Wert, wird diese Version automatisch genommen. Um den Code besser lesbar zu machen, habe ich aber ausdrücklich DataRowVersion mit angegeben.
Etwas anders ist der Sachverhalt, wenn es darum geht, das Suchkriterium für die Aktualisierung festzulegen. Bedenken Sie, dass die Primärschlüsselspalte der Tabelle authors grundsätzlich editierbar ist und nicht ausgeschlossen werden könnte, dass der Anwender auch hier eine Änderung vorgenommen hat. Die entsprechende Datenzeile kann aber nur dann gefunden werden, wenn der Originalwert übermittelt wird. Deshalb ist es unerlässlich, dass die hinter WHERE angegebenen Spalten immer DataRowVersion.Original-Werte enthalten.
In ähnlicher Weise können Sie auch Command-Objekte bereitstellen, die gelöschte und hinzugefügte Datenzeilen beschreiben.
| ' DeleteCommand erzeugen |
| Public Function GetDeleteCommand(ByVal con As SqlConnection) _ |
| As SqlCommand |
| Dim strSQL As String = "DELETE FROM authors WHERE au_id=@ID" |
| Dim cmd As SqlCommand = New SqlCommand(strSQL, con) |
| ' die Parameter der Parameters-Auflistung hinzufügen |
| cmd.Parameters.Add("@ID", SqlDbType.VarChar, 11) |
| Return cmd |
| End Function |
| ' InsertCommand erzeugen |
| Public Function GetInsertCommand(ByVal con As SqlConnection) _ |
| As SqlCommand |
| Dim strSQL As String = _ |
| "INSERT INTO authors (au_id, au_lname, au_fname, contract) " |
| strSQL &= "Values(@ID, @Zuname, @Vorname, @Contract)" |
| Dim cmd As SqlCommand = New SqlCommand(strSQL, con) |
| ' die Parameter der Parameters-Auflistung hinzufügen |
| cmd.Parameters.Add("@ID", SqlDbType.VarChar, 11) |
| cmd.Parameters.Add("@Zuname", SqlDbType.VarChar, 40) |
| cmd.Parameters.Add("@Vorname", SqlDbType.VarChar, 20) |
| cmd.Parameters.Add("@Contract", SqlDbType.Bit) |
| Return cmd |
| End Function |
| Public Function SubmitDeletedRow(ByVal cmd As SqlCommand, _ |
| ByVal row As DataRow) As Integer |
| ' Parameter füllen |
| cmd.Parameters("@ID").Value = row("au_id", _ |
| DataRowVersion.Original) |
| Return cmd.ExecuteNonQuery() |
| End Function |
| Public Function SubmitAddedRow(ByVal cmd As SqlCommand, _ |
| ByVal row As DataRow) As Integer |
| ' Parameter füllen |
| cmd.Parameters("@ID").Value = row("au_id") |
| cmd.Parameters("@Zuname").Value = row("au_lname") |
| cmd.Parameters("@Vorname").Value = row("au_fname") |
| cmd.Parameters("@Contract").Value = row("contract") |
| Return cmd.ExecuteNonQuery() |
| End Function |
Nun müssen wir uns überlegen, wie wir diese drei Methodenpaare benutzen können. Setzen wir voraus, dass wir eine zur Aktualisierung anstehende Datenzeile über row referenzieren, könnte der Aufruf wie folgt lauten:
| Dim cmdUpdate As SqlCommand = GetUpdateCommand(con) |
| Dim countDS As Integer = SubmitUpdatedRow(cmdUpdate, row) |
| If countDS = 1 Then |
| ' Aktualisierung ist gelungen |
Damit ist der Grundstock gelegt. Aber wie kommen wir an die Referenz einer geänderten DataRow?
Liegt ein DataSet vor, das viele Datenzeilen enthält, von denen wir nicht wissen, ob eine einzelne Datenzeile geändert, hinzugefügt oder gelöscht worden ist, gilt es, einen Weg zu finden, um die Datensätze daraufhin zu untersuchen. Hier bietet sich eine Überladung der Methode Select der DataTable an.
| Public Function Select(String, DataRowViewState) As DataRow() |
Der erste Parameter beschreibt eine Zeichenfolge zum Filtern der Datenzeilen, der zweite eine Zeichenfolge für die Sortierrichtung. Der dritte Parameter vom Typ DataRowViewState ist der, der uns die Lösung bietet. DataRowViewState ist eine Enumeration, deren Member bitweise kombiniert werden können.
| Member | Beschreibung |
| Added | Beschreibt eine hinzugefügte Datenzeile. |
| Deleted | Beschreibt eine gelöschte Datenzeile. |
| ModifiedCurrent | Beschreibt eine geänderte Datenzeile. |
Wir können der Select-Methode der DataTable angeben, nach welchen Datenzeilenversionen wir suchen wollen. Handelt es sich um gelöschte, hinzugefügte und editierte, fassen wir das Suchkriterium in einer Variablen zusammen:
| Dim drvs As DataViewRowState = DataViewRowState.Added Or |
| DataViewRowState.Deleted Or |
| DataViewRowState.ModifiedCurrent |
Select liefert ein Array von den Datenzeilen, die sich in irgendeiner Weise vom Original in der Datenbank unterscheiden. Das Array durchlaufen wir elementweise und prüfen dabei jeweils die Eigenschaft RowState der aktuellen DataRow. Je nachdem, ob es sich um eine gelöschte, editierte oder hinzugefügte Zeile handelt, reagieren wir mit dem Aufruf einer unserer drei SubmitXxx-Methoden.
| For Each row As DataRow In ds.Tables(0).Select("", "", drvs) |
| Select Case (row.RowState) |
| Case DataRowState.Added |
| countDS = SubmitAddedRow(cmdInsert, row) |
| Case DataRowState.Deleted |
| countDS = SubmitDeletedRow(cmdDelete, row) |
| Case DataRowState.Modified |
| countDS = SubmitUpdatedRow(cmdUpdate, row) |
| End Select |
| Next |
| If countDS = 1 Then |
| ' Aktualisierung war erfolgreich |
| Else |
| ' Aktualisierung ist fehlgeschlagen |
| End If |
Der Aufruf der Methode ExecuteNonQuery liefert uns als Ergebnis des Aufrufs eine Zahl, welche die Anzahl der betroffenen Datensätze angibt. Da wir für jede einzelne Datenzeile die Methode aufrufen, wird uns die Rückgabe der Zahl 1 signalisieren, ob unser Aktualisierungsversuch von der Originaldatenquelle akzeptiert worden ist. Für uns ist dieses Ergebnis von elementarer Bedeutung, denn wir haben den Aktualisierungsprozess selbst in die Hand genommen und müssen ihn nun auch noch zu einem erfolgreichen Ende bringen.
Die Update-Methode des DataAdapters sorgt automatisch dafür, dass das DataSet nach erfolgreicher Aktualisierung auf den aktuellen Stand gebracht wird. Dazu werden die Daten, die unter DataRowVersion.Original zu finden sind, durch die Daten in DataRowVersion.Current ersetzt. Außerdem wird der RowState der betreffenden Datenzeile angepasst: Zeilen, die Added oder Modified sind, werden auf Unchanged gesetzt. Gelöschte Zeilen werden endgültig aus dem DataSet entfernt.
Ist die Aktualisierung mit ExecuteNonQuery erfolgreich verlaufen, müssen wir das unsererseits ebenfalls im DataSet mitteilen, um den letzten Originalzustand im DataSet widerzuspiegeln. Es gilt demnach, den DataRowState der betreffenden Datenzeilen auf DataRowState.Unchanged zu setzen beziehungsweise erfolgreich gelöschte Datenzeilen aus dem DataSet zu entfernen. Zudem muss bei allen geänderten Datenzeilen DataRowVersion.Current in DataRowVersion.Original übernommen werden. Diese Anpassungen müssen wir jedoch nicht für jede Datenzeile der Reihe nach manuell vornehmen, denn hier hilft uns die Methode AcceptChanges weiter, die auf das DataSet, eine DataTable oder eine DataRow aufgerufen werden kann. Die Änderungen wirken sich nur auf das Objekt aus, auf das die Methode aufgerufen wird.
Klappt die Aktualisierung nicht, wäre der Aufruf von AcceptChanges falsch. Wie in einem solchen Fall reagiert werden kann, behandeln wir später. Zumindest wollen wir aber sicherstellen, dass der Anwender eine Information über den misslungenen Aktualisierungsversuch erhält. Es bietet sich hier an, der Eigenschaft RowError der betreffenden DataRow eine Zeichenfolge zu übergeben, die auf den Fehlversuch hinweist. Gleichzeitig wird die Eigenschaft HasErrors=True gesetzt.
| If countDS = 1 Then |
| row.AcceptChanges() |
| Else |
| row.RowError = "Änderung wurde nicht akzeptiert" |
| End If |
Wir wollen uns nun ein Beispielprogramm ansehen, das unsere insgesamt sechs Methoden dazu benutzt, um Änderungen an den Datenzeilen via ExecuteNonQuery zur pubs-Datenbank zu senden. Im Code werden die Änderungen hartcodiert. Dazu wird der Datensatz, der zu dem Autor White gehört, in der Weise editiert, dass wir den Autor in Black umbenennen. Zudem wird ein neuer Datensatz hinzugefügt, der neben den Feldern au_id, au_lname und au_fname auch in der Spalte contract einen Wert einträgt, da diese keine NULL-Werte erlaubt.
| ' ----------------------------------------------------------------- |
| ' Beispiel: ...\Kapitel 26\ManuelleAktualisierungMitExecuteNonQuery |
| ' ----------------------------------------------------------------- |
| Imports System.Data.SqlClient |
| Module Module1 |
| Sub Main() |
| ' listet die Autoren in der Reihenfolge ihrer Zunamen auf |
| Dim strSQL As String = _ |
| "SELECT au_id, au_lname, au_fname, contract FROM authors" |
| Dim strCon As String = _ |
| "Data Source=(local);Initial Catalog=pubs; " |
| strCon &= "Trusted_Connection=Yes" |
| Dim con As SqlConnection = New SqlConnection(strCon) |
| Dim da As SqlDataAdapter = New SqlDataAdapter(strSQL, con) |
| Dim ds As New DataSet |
| da.Fill(ds) |
| ' Festlegen der Command-Objekte |
| Dim cmdInsert As SqlCommand = GetInsertCommand(con) |
| Dim cmdUpdate As SqlCommand = GetUpdateCommand(con) |
| Dim cmdDelete As SqlCommand = GetDeleteCommand(con) |
| ' Datenzeilen editieren |
| For Each row As DataRow In ds.Tables(0).Rows |
| If (row("au_lname").ToString() = "White") Then |
| row("au_lname") = "Black" |
| End If |
| Next |
| Dim newRow As DataRow = ds.Tables(0).NewRow() |
| newRow("au_id") = "111–11–1111" |
| newRow("au_lname") = "Fischer" |
| newRow("au_fname") = "Fritz" |
| newRow("contract") = 0 |
| ds.Tables(0).Rows.Add(newRow) |
| ' Definition einer Variablen vom Typ DataViewRowState, |
| ' die alle zu berücksichtigenden Änderungen beschreibt |
| Dim drvs As DataViewRowState = DataViewRowState.Added Or _ |
| DataViewRowState.Deleted Or _ |
| DataViewRowState.ModifiedCurrent |
| ' geänderte Datenzeilen in die Datenquelle schreiben |
| Dim countDS As Integer = 0 |
| con.Open() |
| For Each row As DataRow In ds.Tables(0).Select("", "", drvs) |
| Select Case (row.RowState) |
| Case DataRowState.Added |
| countDS = SubmitAddedRow(cmdInsert, row) |
| Console.WriteLine("DS erfolgreich hinzugefügt.") |
| Case DataRowState.Deleted |
| countDS = SubmitDeletedRow(cmdDelete, row) |
| Console.WriteLine("DS erfolgreich gelöscht.") |
| Case DataRowState.Modified |
| countDS = SubmitUpdatedRow(cmdUpdate, row) |
| Console.WriteLine("DS erfolgreich geändert.") |
| End Select |
| If (countDS = 1) Then |
| row.AcceptChanges() |
| Else |
| row.RowError = "Eine Änderung wurde nicht akzeptiert" |
| End If |
| Next |
| Console.WriteLine("Datenbank wurde aktualisiert") |
| con.Close() |
| Console.ReadLine() |
| End Sub |
Sie werden spätestens dann, wenn Sie zweimal hintereinander das Programm ausführen, einen Laufzeitfehler erhalten. Grund ist, dass zum zweiten Mal ein Datensatz mit dem gleichen Primärschlüssel hinzugefügt werden soll. Konflikte dieser Art können natürlich in der Regel nicht unbehandelt bleiben. Unser Code geht aber darauf nicht weiter ein. Weiter unten werden wir uns mit der Konfliktlösung beschäftigen.
Änderungen an den Datenzeilen können sowohl das Löschen als auch das Hinzufügen oder Editieren einer DataRow sein. Liegen gleichzeitig verschiedenartige Änderungen vor und sollen beispielsweise nur alle gelöschten Datenzeilen der Originaldatenbank mitgeteilt werden, bietet sich der Einsatz von ExecuteNonQuery an, wie es im Abschnitt zuvor beschrieben worden ist. Sie stellen damit sicher, dass nicht alle in diesem Fall unerwünschten Änderungen gleichzeitig mitgesendet werden. Wollen Sie das DataSet aber vollständig aktualisieren, gibt es einen eleganteren und auch einfacheren Weg. Er führt über den DataAdapter und dessen Update-Methode.
Die Update-Methode arbeitet im Grunde genommen sehr ähnlich wie der Code, den wir im letzten Abschnitt in Main geschrieben hatte. Sie sucht in einem DataSet bzw. in der DataTable nach den Datenzeilen, deren DataRowState nicht Unchanged ist. Trifft die Methode auf eine in welcher Weise auch immer geänderte Datenzeile, greift sie auf ein entsprechendes Command-Objekt zurück, weist die entsprechenden Parameter zu und setzt die Änderung ab.
Der entscheidende Punkt ist, dass der DataAdapter sich nicht dafür interessiert, wie das Command-Objekt gestaltet ist und aus welcher Quelle es stammt. Wichtig ist ihm nur, dass ein gültiges Command-Objekt vorliegt.
Damit haben wir auch schon den ersten Ansatz gefunden. Wir stellen eigene Command-Objekte zur Verfügung, nennen sie updateCommand, deleteCommand und insertCommand und weisen sie den entsprechenden Eigenschaften des DataAdapters zu:
| <SqlDataAdapter>.UpdateCommand = updateCommand |
| <SqlDataAdapter>.InsertCommand = insertCommand |
| <SqlDataAdapter>.DeleteCommand = deleteCommand |
Alle drei Eigenschaften sind vom Typ SqlCommand. Ein Command-Objekt kennt durch seine Eigenschaft CommandText das SQL-Kommando, das gegen die Datenbank abgesetzt werden soll. Damit sind alle Forderungen, welche die Methode Update des DataAdapters stellt, erfüllt.
Sehen wir uns nun die Methode an, die für das Erzeugen des Kommandos zum Absetzen einer Datenzeilenänderung verantwortlich ist. Auch bei dieser Methode sei angenommen, dass zwischenzeitliche Änderungen durch andere Benutzer bei der Aktualisierung überschrieben werden. Für das Suchkriterium für den Aktualisierungsprozess genügt deshalb auch in diesem Beispiel die Angabe des Primärschlüssels.
| Public Function CreateUpdateCommand(ByVal con As _ |
| SqlConnection) As SqlCommand |
| Dim strSQL As String = "UPDATE authors " |
| strSQL &= "SET au_id = _ |
| @IDNew, au_lname = @Zuname, au_fname = @Vorname " |
| strSQL &= "WHERE au_id=@ID" |
| Dim cmd As SqlCommand = New SqlCommand(strSQL, con) |
| ' die Parameter der Parameters-Auflistung hinzufügen |
| Dim col As SqlParameterCollection = cmd.Parameters |
| col.Add("@IDNew", SqlDbType.VarChar, 11, "au_id") |
| col.Add("@Zuname", SqlDbType.VarChar, 40, "au_lname") |
| col.Add("@Vorname", SqlDbType.VarChar, 20, "au_fname") |
| Dim param As SqlParameter |
| param = col.Add("@ID", SqlDbType.VarChar, 11, "au_id") |
| param.SourceVersion = DataRowVersion.Original |
| Return cmd |
| End Function |
Wird der DataAdapter zur Aktualisierung eingesetzt, muss jedem Parameter mitgeteilt werden, aus welcher Spalte der zu editierenden DataRow der Wert für den betreffenden Parameter abgerufen werden soll, beispielsweise:
| cmd.Parameters.Add("@Vorname", SqlDbType.VarChar, 20, "au_fname") |
Hier teilen wir dem Parameter mit, dass er den Wert aus der Spalte au_fname beziehen soll. Standardmäßig wird der Wert aus DataRowVersion.Current bezogen. Vergessen Sie die Angabe des vierten Parameters, kann der DataAdapter die Datenzeile nicht an die Datenbank übermitteln. Haben Sie eine explizite Referenz auf den Parameter, können Sie die Spalte auch der Eigenschaft SourceColumn bekannt geben.
Die Parameter der Suchkriterien benötigen den Originalwert, um die betreffende Datenzeile in der Tabelle der Datenbank aufzuspüren. Damit der DataAdapter die erforderlichen Werte aus DataRowVersion.Original einträgt, teilen Sie das dem Parameter in seiner Eigenschaft SourceVersion mit, z. B.:
param.SourceVersion = DataRowVersion.Original
Das auf diese Weise in der Methode erzeugte Command-Objekt wird an den Aufrufer zurückgeliefert. Wie Sie weiter oben schon gesehen haben, weisen wir dessen Referenz der Eigenschaft UpdateCommand des DataAdapters zu, der automatisch die Parameter füllt, wenn er auf eine geänderte Datenzeile trifft.
In ähnlicher Weise codieren wir auch die Methoden zum Löschen und Hinzufügen von Datenzeilen.
| Public Function CreateDeleteCommand(ByVal con As _ |
| SqlConnection) As SqlCommand |
| Dim strSQL As String = "DELETE FROM authors WHERE au_id=@ID" |
| Dim cmd As SqlCommand = New SqlCommand(strSQL, con) |
| ' die Parameter der Parameters-Auflistung hinzufügen |
| Dim param As SqlParameter |
| param = cmd.Parameters.Add("@ID", SqlDbType.VarChar, 11, "au_id") |
| param.SourceVersion = DataRowVersion.Original |
| Return cmd |
| End Function |
| Public Function CreateInsertCommand(ByVal con As _ |
| SqlConnection) As SqlCommand |
| Dim strSQL As String = _ |
| "INSERT INTO authors (au_id, au_lname, au_fname, contract) " |
| strSQL &= "Values(@ID, @Zuname, @Vorname, @Contract)" |
| Dim cmd As SqlCommand = New SqlCommand(strSQL, con) |
| ' die Parameter der Parameters-Auflistung hinzufügen |
| cmd.Parameters.Add("@ID", SqlDbType.VarChar, 11, "au_id") |
| cmd.Parameters.Add("@Zuname", SqlDbType.VarChar, 40, "au_lname") |
| cmd.Parameters.Add("@Vorname", SqlDbType.VarChar, 20, "au_fname") |
| cmd.Parameters.Add("@Contract", SqlDbType.Bit, 2, "contract") |
| Return cmd |
| End Function |
Planen Sie die manuelle Aktualisierung unter Zuhilfenahme des DataAdapters, unterscheidet sich der Programmcode kaum von dem, den Sie auch unter Benutzung des CommandBuilders schreiben würden. Anstatt den CommandBuilder zu erzeugen, weisen Sie nur den Eigenschaften InsertCommand, DeleteCommand und UpdateCommand des DataAdapters die richtigen Command-Objekte zu und rufen anschließend Update auf. Das ist bereits alles.
Das folgende Beispielprogramm zeigt Ihnen die Vorgehensweise. Wie schon im Beispiel zuvor wird ein Datensatz hinzugefügt und der Autor White umbenannt. Sie sollten, bevor Sie das Beispielprogramm ausprobieren, etwaige Änderungen, die aus dem letzten Beispiel stammen, in der Originaldatenbank zurücksetzen. Löschen Sie also den Datensatz mit der ID 111 – 11 – 1111, und lassen Sie den Autor White auch wirklich so heißen. Er könnte noch Black sein.
| ' -------------------------------------------------------------- |
| ' Beispiel: ...\Kapitel 26\\ManuellesAktualisierenMitDataAdapter |
| ' -------------------------------------------------------------- |
| Imports System.Data.SqlClient |
| Module Module1 |
| Sub Main() |
| ' listet die Autoren in der Reihenfolge ihrer Zunamen auf |
| Dim strSQL As String = _ |
| "SELECT au_id, au_lname, au_fname, contract FROM authors" |
| Dim strCon As String = "Data Source=(local);Initial Catalog=pubs;Trusted_Connection=Yes" |
| Dim con As SqlConnection = New SqlConnection(strCon) |
| Dim da As SqlDataAdapter = New SqlDataAdapter(strSQL, con) |
| Dim ds As New DataSet |
| da.Fill(ds) |
| ' Datenzeilen editieren |
| For Each row As DataRow In ds.Tables(0).Rows |
| If (row("au_lname").ToString() = "White") Then |
| row("au_lname") = "Black" |
| End If |
| Next |
| ' Datenzeilen hinzufügen |
| Dim newRow As DataRow = ds.Tables(0).NewRow() |
| newRow("au_id") = "111–11–1111" |
| newRow("au_lname") = "Fischer" |
| newRow("au_fname") = "Fritz" |
| newRow("contract") = 0 |
| ds.Tables(0).Rows.Add(newRow) |
| ' Festlegen der Command-Objekte |
| da.InsertCommand = CreateInsertCommand(con) |
| da.UpdateCommand = CreateUpdateCommand(con) |
| da.DeleteCommand = CreateDeleteCommand(con) |
| da.Update(ds) |
| End Sub |
| End Module |
Ein DataSet enthält einen Teilausschnitt einer Datenbank. Sie können sich die Daten anzeigen lassen, Sie können sie aber auch ändern, neue Datensätze hinzufügen oder Datensätze löschen. Sie wissen inzwischen auch, dass Sie eine Aktualisierung auf einfache Weise mit einem CommandBuilder in die Wege leiten können. Das ist ausgesprochen simpel zu programmieren und funktioniert tadellos. Ich habe Ihnen zudem gezeigt, wie Sie ohne den CommandBuilder eine eigene Aktualisierungslogik bereitstellen können. Der Code ist vielleicht spannend, aber andererseits könnten Sie auch zu der Erkenntnis kommen, dass die investierte Zeit nicht besonders effektiv genutzt worden ist.
Dem ist bei weitem nicht so. Wahrscheinlich werden Sie sogar sehr häufig eine eigene Aktualisierungslogik generieren müssen. Ich möchte Ihnen das am Beispiel des CommandBuilders beweisen. Gehen wir davon aus, dass einer Abfrage das folgende SELECT-Statement zugrunde liegt:
| SELECT au_id, au_lname, au_fname FROM authors |
Auf dieser Grundlage wird der CommandBuilder ein UpdateCommand erzeugen, das wie folgt aussieht:
| UPDATE authors SET au_id=@p1, au_lname=@p2, au_fname=@p3 |
| WHERE au_id=@p4 AND au_lname=@p5 AND au_fname=@p6 |
In der WHERE-Klausel der UPDATE-Anweisung sind alle Spalten enthalten, die in SELECT angegeben worden sind. Die Parameter @p4 bis @p6 werden bei der Aktualisierung mit den Werten aus DataRowVersion.Original gefüttert.
Nehmen wir nun an, der Benutzer der Datenbankanwendung hätte einen Datensatz mit den folgenden Feldinhalten bezogen:
| au_id = "123–12–1234" |
| au_lname = "Müller" |
| au_fname = "Peter" |
Ändert der Anwender den Zunamen in »Meier«, wird der CommandBuilder folgendes UPDATE-Statement erzeugen:
| UPDATE authors SET au_id="123–12–1234", au_lname="Meier", au_fname="Peter" |
| WHERE au_id=@p4 AND au_lname="Müller" AND au_fname="Peter" |
Auf der Datenbank wird nun ein Datensatz in der Autorentabelle gesucht, der genau den Kriterien entspricht, welche die WHERE-Klausel beschreibt. Wird der Datensatz gefunden, werden in den in SET aufgeführten Spalten die entsprechenden Werte eingetragen. Die Aktualisierung war erfolgreich.
Zu glauben, dass das immer so sein wird, ist utopisch, wenn mehrere Anwender ihre Änderungen an die Datenbank übermitteln können. Nehmen wir an, Anwender A ändert wie gezeigt in seinem DataSet den Datensatz, während Anwender B ebenfalls eine Änderung am gleichen Datensatz vornimmt, jedoch nicht in der Spalte au_lname, sondern in der Spalte au_fname. Aktualisiert Anwender B zuerst, werden seine editierten Daten problemlos in die Datenbank geschrieben. Versucht anschließend Anwender A, seine Änderungen zu übermitteln, schlägt der Versuch fehl, weil der Datensatz, der überschrieben werden soll, nicht mehr gefunden wird.
Merken Sie, worauf meine Ausführungen hinauslaufen? Sie müssen schon im Vorfeld der Entwicklung klären, wie Sie unter Berücksichtigung mehrerer Benutzer die Datenbank aktualisieren wollen. Je nach Ausgangssituation und den angegebenen Suchkriterien werden die Daten entweder erfolgreich in die Datenbank geschrieben, oder die Aktualisierung verursacht einen Konflikt.
Grundsätzlich können wir hinsichtlich der Kriterien zur Identifizierung eines zur Aktualisierung anstehenden Datensatzes zwischen drei Fällen unterscheiden:
| Die WHERE-Klausel enthält alle Spalten der SELECT-Abfrage. |
| Die WHERE-Klausel enthält die Primärschlüsselspalte. |
| Die WHERE-Klausel enthält nur die Angabe der geänderten Spalten und der Primärschlüsselspalte. |
Wir sollten uns nun mit den Konsequenzen dieser drei Varianten vertraut machen, denn die Entscheidung hat auch Einfluss darauf, wie wir mit den eventuell auftretenden Konflikten umgehen und sie lösen.
Standardmäßig nimmt der CommandBuilder alle Spalten in der WHERE-Klausel auf, die mit SELECT abgefragt worden sind. Die Folge ist, dass der Code keine Änderungen in einer DataRow überschreiben kann, die zwischenzeitlich von anderen Anwendern vorgenommen worden sind.
Dazu ein Beispiel. Angenommen, Anwender A und Anwender B rufen die gleiche Datenzeile in der Autorentabelle ab. Ändert Anwender A die Spalte au_lname, werden alle Spalten der Abfrage in die WHERE-Klausel aufgenommen. Das UPDATE-Statement könnte dann beispielsweise wie folgt aussehen:
| UPDATE authors |
| SET au_id="172–32–1176", au_lname="Black", city="Oakland" |
| WHERE au_id="172–32–1176", au_lname="White", city="Oakland" |
In der Zwischenzeit könnte auch Anwender B die Datenzeile mit dem Autor geändert haben, jedoch nicht wie Anwender A die Spalte au_lname, sondern die Spalte city.
| UPDATE authors |
| SET au_id="172–32–1176", au_lname="White", city="New York" |
| WHERE au_id="172–32–1176", au_lname="White", city="Oakland" |
Hat Anwender A seine Änderungen zuerst übermittelt, scheitert der Aktualisierungsversuch von Anwender B, weil keine Zeile in der Tabelle den Kriterien der WHERE-Klausel entspricht.
Bei diesem Szenario »gewinnt« immer der Anwender, der als erster seine Änderungen an die Datenbank übermittelt. Der Anwender, der seine Änderungen später zur Datenbank schickt, hat das Nachsehen. Sein Aktualisierungsversuch misslingt.
Aktualisierungsszenarien, die anders ablaufen sollen als das zuvor beschriebene, erfordern das Bereitstellen benutzerdefinierter Command-Objekte. Ein denkbarer Ansatz wäre der, dass jede Aktualisierung grundsätzlich immer in den entsprechenden Datensatz der Datenbank geschrieben wird. Zwischenzeitliche Änderungen durch andere Anwender werden ohne Kenntnis darüber, was ein zweiter Anwender geändert hat, überschrieben. Deshalb wird diese Art der Aktualisierung auch als »Last in wins« bezeichnet.
Bei dieser Form der Aktualisierung muss nur der betroffene Datensatz eindeutig identifiziert werden. Deshalb wird hinter der WHERE-Klausel auch nur der Primärschlüssel der Datenzeile angegeben.
Halten wir uns nun die Situation vor Augen. Wieder helfen uns die beiden fiktiven Anwender A und B dabei, den Sachverhalt zu verstehen. Beide Anwender rufen parallel die gleiche Datenzeile ab und nehmen Änderungen an einer der Spalten vor. Anwender A aktualisiert die Originaldatenbank zuerst. Wie das UPDATE-Statement aussieht, spielt dabei noch nicht einmal eine Rolle. Anwender B übermittelt seine Änderung erst später. Nehmen wir an, Anwender B hätte den Inhalt in der Spalte city verändert, könnte sein vollständiges Aktualisierungsstatement wie folgt lauten:
| UPDATE authors |
| SET au_id="172–32–1176", au_lname="White", city="New York" |
| WHERE au_id="172–32–1176" |
Die Aktualisierung wird erfolgreich sein, wenn der Datensatz mit der angegebenen au_id in der Tabelle gefunden wird. Die Änderungen von Anwender A sieht Anwender B nicht und wird vielleicht auch niemals erfahren, welche Daten Anwender A überschrieben hat. Die Identifizierung der zu ändernden Datenzeile nur anhand der Primärschlüsselspalte ist folglich auch denkbar ungeeignet, wenn Sie vermeiden müssen, dass Anwender B unwissentlich geänderte Daten überschreibt. Können Sie davon ausgehen, dass die letzte Aktualisierung zweifelsfrei diejenige mit den »besten« Daten ist, sollten Sie sich für diese Alternative entscheiden.
Die beiden vorhergehend beschriebenen Szenarien sind ausgesprochen gegensätzlich: Entweder wird eine vorhergehende Aktualisierung kompromisslos überschrieben, oder der Anwender, der als zweiter versucht, seine Änderungen zu übermitteln, hat sich mit einem Konflikt auseinander zu setzen.
Dadurch, dass Sie mit ADO.NET selbst die Aktualisierungslogik bereitstellen können, können Sie auch einen Kompromiss schließen und in der WHERE-Klausel neben der Primärschlüsselspalte auch die Spalten angeben, die der Anwender selbst verändert hat.
Eine kurze Beschreibung der Situation. Anwender A und Anwender B rufen gleichzeitig die gleiche Zeile mit Autorendaten ab. Anwender A ändert die Spalte au_lname, Anwender B die Spalte city. Anwender A übermittelt als erster seine Änderungen. Das UPDATE-Statement wie folgt aus:
| UPDATE authors |
| SET au_lname="Black" |
| WHERE au_id="172–32–1176" AND au_lname="White", |
Bei den Spalteninhalten, die hinter WHERE aufgeführt sind, handelt es sich um die Originaldaten, die der Anwender beim Füllen des DataSets erhalten hat.
Die UPDATE-Abfrage, die Anwender B nach Anwender A an die Datenbank sendet, sucht aber nach anderen Gesichtspunkten den zu aktualisierenden Datensatz:
| UPDATE authors |
| SET city="Miami" |
| WHERE au_id="172–32–1176" AND city="Oakland" |
Der Datensatz kann auch bei der später erfolgenden Aktualisierung immer noch eindeutig in der Tabelle der Autoren identifiziert werden. Damit werden auch die Änderungen, die Anwender B vorgenommen hat, in die Tabelle geschrieben, ohne dass die Änderungen von Anwender A überschrieben werden. Beide kommen zu ihrem Recht, solange sie unterschiedliche Spalten editiert haben.
Es sollte an dieser Stelle erwähnt werden, dass die Aktualisierungslogik des Command-Objekts dynamisch zur Laufzeit erzeugt werden muss, da Sie nicht wissen, welche Spalten von den Änderungen des jeweiligen Benutzers betroffen sind. Hierzu vergleichen Sie DataRowVersion.Current und DataRowVersion.Original jeder Spalte einer Datenzeile und »schrauben« sich auf diese Weise die Zeichenfolge des UDATE-Befehls zusammen, bevor Sie das Command–Objekt damit füttern. Die Codierung dazu kann, je nachdem, wie viele Spalte die Datenzeile hat, relativ aufwändig werden. Das ist der große Nachteil dieser Aktualisierung.
Einen Tipp zu geben, welche Suchkriterien zu bevorzugen sind, ist nicht möglich. Sie müssen genau analysieren, welche Vorstellungen Sie oder Ihr Kunde haben. Wahrscheinlich werden Sie damit sogar heftige Diskussionen auslösen, bevor die endgültige Entscheidung fällt. Aber das liegt in der Natur der Sache, denn jede Datenbank ist spezifisch, und jede Datenbankanwendung muss anderen Kriterien genügen.
Wichtig ist es zu wissen, dass Sie mit ADO.NET ein Werkzeug in den Händen halten, das alle Möglichkeiten offen lässt. Wenn Sie Glück haben, genügt die Aktualisierungslogik des CommandBuilders den Ansprüchen. Dann wird die Aktualisierung zu einem Kinderspiel. Wenn nicht, erzeugen Sie ein UPDATE-Statement nach den Vorgaben, die an die Anwendung gestellt werden.
Stehen mehrere Datenzeilen zur Aktualisierung an, wird der DataAdapter versuchen, eine nach der anderen an die Datenbank zu senden. Wie aber wird sich der DataAdapter verhalten, wenn eine der Datenzeilen einen Konflikt verursacht? Der DataAdapter wird eine DBConcurrencyException auslösen und die verbleibenden Änderungen nicht mehr an die Datenbank schicken. So ist das Standardverhalten.
Sie können den DataAdapter auch anweisen, nach einem etwaigen Konflikt seine Aufgabe fortzusetzen und auch die verbleibenden Änderungen zu übermitteln. Dazu setzen Sie die Eigenschaft ContinueUpdateOnError=True. Das hat weit reichende Konsequenzen, denn nun verursacht ein fehlgeschlagener Aktualisierungsversuch keine Ausnahme mehr. Stattdessen wird die Eigenschaft HasErrors des entsprechenden DataRow-Objekts auf True gesetzt, ebenso die gleichnamige Eigenschaft des DataSets und der DataTable. Eine DataRow hat auch eine Eigenschaft RowError. Diese enthält nach dem misslungenen Versuch eine Fehlermeldung.
Im folgenden Beispielcode wird der Einsatz der Eigenschaften ContinueUpdateOnError, HasErrors und RowError gezeigt. Die Command-Objekte werden von den schon bekannten Methoden CreateUpdateCommand, CreateDeleteCommand und CreateInsertCommand generiert. Um eine etwas größere »Spielwiese« zu haben, ist das Suchkriterium von CreateUpdateCommand um eine Spalte (au_lname) ergänzt worden.
Geändert wird erneut der Datensatz des Autors White in der Tabelle authors. Sorgen Sie deshalb vor dem ersten Ausführen dafür, den Ursprungszustand der Tabelle wieder herzustellen. Außerdem wird erneut ein Datensatz zur Tabelle hinzugefügt. In Main wird die laufende Anwendung nach dem Füllen des DataSets unterbrochen. Das ist der Moment, um beispielsweise mit dem Enterprise Manager des SQL Servers den Datensatz des Autors White zu editieren. Damit simulieren Sie einen zweiten Anwender. Ändern Sie dabei die Spalte au_lname oder au_id, provozieren Sie den Parallelitätskonflikt. Es erscheint an der Konsole eine Fehlermeldung. Editieren Sie jedoch eine andere Spalte, werden beide Aktualisierungen von der Datenbank akzeptiert.
| ' ---------------------------------------------------------- |
| ' Beispiel: ...\Kapitel 26\Konfliktbeschreibung |
| ' ---------------------------------------------------------- |
| Imports System.Data.SqlClient |
| Module Module1 |
| Sub Main() |
| ' listet die Autoren in der Reihenfolge ihrer Zunamen auf |
| Dim strSQL As String = _ |
| "SELECT au_id, au_lname, au_fname, contract FROM authors" |
| Dim strCon As String = "..." |
| Dim con As SqlConnection = New SqlConnection(strCon) |
| Dim da As SqlDataAdapter = New SqlDataAdapter(strSQL, con) |
| Dim ds As New DataSet |
| da.Fill(ds) |
| ' Hier wird angehalten, um einen Konflikt zu provozieren |
| Console.Write("Ändern Sie jetzt in der Originaldatenbank.") |
| Console.WriteLine("Weiter mit <Enter>.") |
| Console.ReadLine() |
| ' Datenzeilen editieren |
| For Each row As DataRow In ds.Tables(0).Rows |
| If row("au_lname").ToString() = "White" Then |
| row("au_lname") = "Black" |
| End If |
| Next |
| Dim newRow As DataRow = ds.Tables(0).NewRow() |
| newRow("au_id") = "111–11–1111" |
| newRow("au_lname") = "Fischer" |
| newRow("au_fname") = "Fritz" |
| newRow("contract") = 0 |
| ds.Tables(0).Rows.Add(newRow) |
| ' Festlegen der Command-Objekte |
| da.InsertCommand = CreateInsertCommand(con) |
| da.UpdateCommand = CreateUpdateCommand(con) |
| da.DeleteCommand = CreateDeleteCommand(con) |
| da.ContinueUpdateOnError = True |
| da.Update(ds) |
| ' Prüfen, ob ein Konflikt aufgetreten ist |
| If ds.HasErrors Then |
| Dim message As String = _ |
| "Folgende Zeilen konnten nicht aktualisiert werden:" |
| For Each row As DataRow In ds.Tables(0).Rows |
| If row.HasErrors Then |
| Console.WriteLine(message) |
| Console.WriteLine("ID: {0}, Fehler: {1}", _ |
| row("au_id"), row.RowError) |
| End If |
| Next |
| Else |
| Console.WriteLine("Die Aktualisierung war erfolgreich.") |
| End If |
| Console.ReadLine() |
| End Sub |
| Public Function CreateUpdateCommand(ByVal con As _ |
| SqlConnection) As SqlCommand |
| Dim strSQL As String = "UPDATE authors " & _ |
| "SET au_id=@IDNew,au_lname=@Zuname, au_fname=@Vorname " & _ |
| "WHERE au_id=@ID AND au_lname=@ZunameOld" |
| Dim cmd As SqlCommand = New SqlCommand(strSQL, con) |
| ' die Parameter der Parameters-Auflistung hinzufügen |
| Dim col As SqlParameterCollection = cmd.Parameters |
| col.Add("@IDNew", SqlDbType.VarChar, 11, "au_id") |
| col.Add("@Zuname", SqlDbType.VarChar, 40, "au_lname") |
| col.Add("@Vorname", SqlDbType.VarChar, 20, "au_fname") |
| Dim param As SqlParameter |
| param = col.Add("@ID", SqlDbType.VarChar, 11, "au_id") |
| param.SourceVersion = DataRowVersion.Original |
| param = col.Add("@ZunameOld", SqlDbType.VarChar, 40, "au_lname") |
| param.SourceVersion = DataRowVersion.Original |
| Return cmd |
| End Function |
| End Module |
Nach dem Aufruf von Update auf den DataAdapter wird zuerst mit
| If (ds.HasErrors Then |
das DataSet dahingehend untersucht, ob überhaupt ein Konflikt vorliegt. HasErrors ist False, wenn die Datenbank die Änderungen angenommen hat. True signalisiert hingegen, dass wir alle Datenzeilen in der Tabelle des DataSets durchlaufen müssen, um die konfliktverursachenden Zeilen zu finden. Fündig wird der Code, wenn er auf eine Datenzeile mit HasErrors=True trifft.
| For Each row As DataRow in ds.Tables(0).Rows |
| If row.HasErrors Then |
| ... |
| End If |
| Next |
Jetzt können wir reagieren. Im einfachsten Fall lassen wir uns zumindest die ID des betreffenden Übeltäters ausgeben – so wie in diesem Beispiel. Sie können die Information natürlich auch dazu benutzen, dem Benutzer die Chance zu geben, die Konfliktursache zu beseitigen, denn der Verursacher ist ermittelt.
Meist genügt es nicht nur zu wissen, wer Konfliktverursacher ist. Es wird darüber hinaus auch eine Lösung angestrebt. Das bedarf aber einer genaueren Analyse der Umstände. Ich möchte Ihnen das kurz erläutern.
Wird versucht, einen bereits vorhandenen Primärschlüssel für einen neuen Datensatz ein zweites Mal zu vergeben, scheint die Lösung des Problems noch recht einfach zu sein: Es muss dann nur ein anderer Primärschlüssel vergeben werden. Aber das könnte eine falsche Entscheidung sein. Können Sie denn sicherstellen, dass nicht zwei Anwender versuchen, den gleichen Datensatz zur Tabelle hinzuzufügen? Wenn Sie diese Situation nicht berücksichtigen, liegen im schlimmsten Fall zwei identische Datensätze vor.
Weiterhin stellt sich auch die Frage, warum eine geänderte Datenzeile nicht erfolgreich aktualisiert werden konnte. Die Ursache könnte sein, dass ein anderer Anwender seinerseits den Datensatz geändert hat, es könnte aber auch sein, dass der Datensatz gar nicht mehr existiert, weil er in der Originaltabelle gelöscht worden ist.
Vielleicht merken Sie, es fehlt uns bisher an Informationen, um präzise Lösungen erarbeiten und codieren zu können. Der Ansatz zur Lösungsfindung ist aber nicht im aktuellen DataSet zu finden, sondern in der Datenbank selbst. Was wir brauchen, ist eine neue Originalversion der konfliktverursachenden Datenzeile.
Der DataAdapter hilft uns weiter. Er löst nämlich für jede zu aktualisierende Datenzeile zwei Ereignisse aus, wenn anstehende Änderungen über die Methode Update an die Datenbank übermittelt werden:
| RowUpdating |
| RowUpdated |
RowUpdating wird ausgelöst, bevor eine Zeile übermittelt wird, RowUpdated tritt unmittelbar nach der Übermittlung auf. Für unsere Lösung untersuchen wir im Ereignishandler nach der Übermittlung den zweiten Parameter vom Typ SqlRowUpdatedEventArgs. Mehrere Eigenschaften dieses Typs unterstützen uns bei unserem weiteren Vorgehen.
| Eigenschaft | Beschreibung |
| Command | Ruft das beim Aufruf von Update ausgeführte SqlCommand ab. |
| Errors | Ruft alle Fehler ab, die während der Ausführung generiert wurden. |
| RecordsAffected | Ruft die Anzahl der durch die Ausführung der SQL-Anweisung geänderten, eingefügten oder gelöschten Zeilen ab. |
| Row | Ruft die durch ein Update gesendete DataRow ab. |
| StatementType | Ruft den Typ der ausgeführten SQL-Anweisung ab. |
| Status | Ruft einen Wert der Enumeration UpdateStatus ab oder legt diesen fest. |
| TableMapping | Ruft das durch ein Update gesendete DataTableMapping ab. |
Der Vollständigkeit halber folgt jetzt auch noch die Tabelle mit den Membern der Enumeration UpdateStatus, die von der Eigenschaft Status des SqlRowUpdatedEventArgs-Objekts offengelegt wird.
| Member | Beschreibung |
| Continue | Der DataAdapter soll mit der Verarbeitung von Zeilen fortfahren. |
| ErrorsOccured | Der Ereignishandler meldet, dass die Aktualisierung als Fehler behandelt werden soll. |
| SkipAllRemainingRows | Die aktuelle Zeile und alle restlichen Zeilen sollen nicht aktualisiert werden. |
| SkipCurrentRow | Die aktuelle Zeile soll nicht aktualisiert werden. |
Wie können wir nun das Ereignis zu unserem Nutzen einsetzen?
Es gilt zunächst einmal festzustellen, ob das Updaten einer Datenzeile zu einem Konflikt geführt hat. Hierzu prüfen wir, ob die Eigenschaft Status des SqlRowUpdatedEventArgs-Objekts den Enumerationswert UpdateStatus.ErrorsOccured aufweist.
| Private Sub da_RowUpdated(ByVal sender As Object, _ |
| e As SqlRowUpdatedEventArgs) |
| If e.Status = UpdateStatus.ErrorsOccurred Then |
| ... |
| End If |
| End Sub |
Alle konfliktverursachenden Datenzeilen können in einem DataSet zusammengefasst werden, das sich nach kompletter Aktualisierung auswerten lässt. Dazu muss der aktuelle Stand der konfliktverursachenden Datenzeilen aus der Originaldatenbank bezogen werden, damit Benutzer oder Programmcode die Basis zu einer möglichen Entscheidung hinsichtlich einer Konfliktlösung haben. Im Ereignishandler wird deshalb eine parametrisierte Abfrage, die gegen die Datenbank abgesetzt werden soll, zusammengeschraubt (aber zunächst noch nicht ausgeführt). Sinnvollerweise wird dabei als Parameter nur der Primärschlüssel der konfliktverursachenden Datenzeile verwendet. Abgefragt werden von der Datenbank die Inhalte aller im Zusammenhang mit der Aktualisierung interessierenden Spalten.
| Dim strSQL As String = "SELECT au_id, au_lname, au_fname, contract " |
| strSQL &= "FROM authors WHERE au_id=@ID" |
| Dim cmd As SqlCommand = New SqlCommand(strSQL, con) |
| cmd.Parameters.Add("@ID", SqlDbType.VarChar, 11, "au_id") |
| daConflict.SelectCommand = cmd |
| daConflict.SelectCommand.Parameters(0).Value = e.Row("au_id") |
daConflict ist hierbei die Referenz auf ein SqlDataAdapter-Objekt.
Grundsätzlich können beim Aktualisieren einer Datenzeile zwei verschiedene Exceptions auftreten:
| SqlException |
| DBConcurrencyException |
SqlException beschreibt eine Ausnahme, wenn SQL Server einen Fehler zurückgibt. Das wäre beispielsweise der Fall, wenn ein Datensatz hinzugefügt wird mit einem Primärschlüssel, der in der Tabelle bereits existiert.
DBConcurrencyException hingegen wird ausgelöst, wenn eine Parallelitätsverletzung vorliegt. Das ist der Fall, wenn die Rückgabe der Anzahl der aktualisierten Datenzeilen 0 ist.
Um festzustellen, welche Ausnahme ausgelöst worden ist, brauchen Sie nur den in der Eigenschaft Errors des SqlRowUpdatedEventArgs-Objekts enthaltenen Ausnahmetyp zu untersuchen. Ist dieser bekannt, können Sie die parametrisierte Abfrage starten. Die Anzahl der zurückgelieferten Datenzeilen, es kann sich dabei nur um eine oder keine handeln, lässt weitere Rückschlüsse auf das vorherige Scheitern der Aktualisierung zu.
Betrachten wir zuerst den Fall, dass Errors ein SqlException-Objekt enthält.
| If e.Errors.GetType() = GetType(SqlException) Then |
| If (daConflict.Fill(dsConflict) = 1) Then |
| Console.WriteLine(e.Row.RowError = "Der PS existiert bereits.") |
| End If |
| End If |
Es wird die Fill-Methode des DataAdapters aufgerufen, um das Konfliktdataset zu füllen. Die parametrisierte Abfrage enthält dabei in ihrem Parameter @ID den Primärschlüssel der Datenzeile, die den Konflikt verursacht hat. Zur Erinnerung, es handelt sich dabei um den Primärschlüssel eines Datensatzes, der neu hinzugefügt werden soll, wobei der Verdacht nahe liegt, dass der Primärschlüssel bereits in der Originaltabelle vergeben ist. Wird die parametrisierte Abfrage eine Datenzeile zurückliefern, bestätigt sich der Verdacht.
Handelt es sich in Errors um DBConcurrencyException, wurde der Versuch, die Änderung an einer Datenzeile in die Originaltabelle zu schreiben, abgelehnt. Die Parallelitätsverletzung kann zwei Ursachen haben:
| Ein anderer Anwender hat den Datensatz zwischenzeitlich geändert. |
| Der Datensatz wurde von einem anderen Anwender gelöscht. |
Festzustellen, welcher der beiden Punkte zum Konflikt geführt hat, ist sehr einfach, denn nach dem Absetzen der parametrisierten Abfrage muss nur der Rückgabewert untersucht werden. Ist das Ergebnis 0, d. h., der entsprechende Datensatz wurde anhand des Primärschlüssels nicht gefunden, existiert er nicht mehr, er wurde gelöscht.
| If TypeOf(e.Errors) Is DBConcurrencyException Then |
| If daConflict.Fill(dsConflict) = 1 Then |
| Console.WriteLine("Ein anderer User hat den Datensatz geändert.") |
| Else |
| Console.WriteLine("Datensatz existiert nicht in der Datenbank.") |
| End If |
| End If |
Wie mit dem Inhalt des DataSets umgegangen wird, das den aktuellen Stand der konfliktverursachenden Zeilen enthält, richtet sich nach den individuellen Bedürfnissen des Kunden, der die Anwendung einsetzt. Die Lösung kann sehr unterschiedlich ausfallen und dabei auch noch sehr komplex sein. Wir wollen daher an dieser Stelle darauf verzichten und uns stattdessen die entscheidenden Ansätze in einem Beispielprogramm im Zusammenhang ansehen.
| ' ---------------------------------------------------------- |
| ' Beispiel: ...\Kapitel 26\KonfliktDataset |
| ' ---------------------------------------------------------- |
| Imports System.Data.SqlClient |
| Module Module1 |
| Dim daConflict As New SqlDataAdapter |
| Dim dsConflict As New DataSet |
| Dim con As SqlConnection |
| Sub Main() |
| ' listet die Autoren in der Reihenfolge ihrer Zunamen auf |
| Dim strSQL As String = _ |
| "SELECT au_id, au_lname, au_fname, contract FROM authors" |
| Dim strCon As String = "..." |
| con = New SqlConnection(strCon) |
| Dim da As SqlDataAdapter = New SqlDataAdapter(strSQL, con) |
| Dim ds As New DataSet |
| da.Fill(ds) |
| ' Datenzeilen editieren |
| For Each row As DataRow In ds.Tables(0).Rows |
| If row("au_lname").ToString() = "White" Then |
| row("au_lname") = "Black" |
| End If |
| Next |
| Dim newRow As DataRow = ds.Tables(0).NewRow() |
| newRow("au_id") = "111–11–1111" |
| newRow("au_lname") = "Fischer" |
| newRow("au_fname") = "Fritz" |
| newRow("contract") = 0 |
| ds.Tables(0).Rows.Add(newRow) |
| ' Aktualisierung vorbereiten |
| Console.WriteLine("Jetzt das au_lname-Feld des " & _ |
| "Datensatzes im SQL-Server ändern.") |
| Console.ReadLine() |
| da.UpdateCommand = CreateUpdateCommand(con) |
| da.InsertCommand = CreateInsertCommand(con) |
| da.DeleteCommand = CreateDeleteCommand(con) |
| da.ContinueUpdateOnError = True |
| AddHandler da.RowUpdated, AddressOf da_RowUpdated |
| da.Update(ds) |
| Console.ReadLine() |
| End Sub |
| ' Wird beim Versuch der Datensatzänderung ausgelöst |
| Sub da_RowUpdated(ByVal sender As Object, _ |
| ByVal e As SqlRowUpdatedEventArgs) |
| If (e.Status = UpdateStatus.ErrorsOccurred) Then |
| ' Vorbereitung auf die Datenbankabfrage |
| Dim strSQL As String = _ |
| "SELECT au_id, au_lname, au_fname, contract " |